/**************************************************************************
 *
 *  THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
 *  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
 *  PURPOSE.
 *
 *  Copyright (C) 1992 - 1997 Microsoft Corporation.  All Rights Reserved.
 *
 **************************************************************************/
/****************************************************************************
 *
 *  WRITEAVI.C
 *
 *  Creates the file OUTPUT.AVI, an AVI file consisting of a rotating clock
 *  face.  This program demonstrates using the functions in AVIFILE.DLL
 *  to make writing AVI files simple.
 *
 *  This is a stripped-down example; a real application would have a user
 *  interface and check for errors.
 *
 ***************************************************************************/
/////////////////////////////////////////////////////////////////////////////
//
// This code has been modified to work with FinalBurn Alpha.
//
//---------------------------------------------------------------------------
//
// AVI Recorder for FBA version 0.6a by Gangta
//
// Limitations:
//
//  - Supported bitdepths are 15, 16, 24, and 32.
//
//  - Avi will be recorded at original game resolution.
//
//  - Video effects will not be recorded.
//    (i.e. stretch, scanline, 3D effects, software effects)
//
//  - Audio compression is not supported.
//
//---------------------------------------------------------------------------
//
// Version History:
//
// 0.6a - added 32-bit clockwise rotation (updated by regret)
//
// 0.6  - added support clockwise rotation (except 24/32 bit depth)
//      - fixed rotation for some flipped games
//        (updated by regret)
//
// 0.5  - fixed a minor bug regarding video stream header
//        (some applications chose wrong decompressor while opening avi)
//
// 0.4  - fixed avi video/audio desync in cave games
//
// 0.3  - fixed the garbled buttom line in resulting avi
//
// 0.2  - interleaved audio
//      - fixed rotation
//      - fixed 24-bit mode
//      - cleaned up some comments, re-wrote some codes
//
// 0.1  - initial release
//
//---------------------------------------------------------------------------
//
// Credits:
//
// - FBA Team (http://fba.emuunlim.com)
//
//   for helping development and making cygwin build possible
//
// - VBA Team (http://vboy.emuhq.com)
//
//   part of this code is incorporated from VBA source
//
// - {Gambit}
//
//   for testing and reporting bugs
//
/////////////////////////////////////////////////////////////////////////////

#include "burner.h"

#ifdef _MSC_VER
 #include <vfw.h> // use the headers from MS Platform SDK
#endif
#ifdef __GNUC__
 #include "vfw.h" // use the modified idheaders included in the src
#endif

#ifdef _MSC_VER
#pragma comment(lib, "vfw32")
#endif

// defines
#define FBAVI_VFW_INIT 0x0001	// avi file library initialized
#define FBAVI_VID_SET  0x0002	// avi video compression set
#define FBAVI_AUD_SET  0x0004	// avi audio compression set (not used for now)

int aviStatus = 0;				// 1 (recording started), 0 (recording stopped)
int aviIntAudio = 0;			// 1 (interleave audio), 0 (do not interleave)
static unsigned char* aviBuffer = NULL;	// pointer to raw pixel data
static int aviFlags = 0;

static struct FBAVI {
	PAVIFILE pFile;				// avi file
	// formats
	BITMAPINFOHEADER bih;		// video format
	WAVEFORMATEX wfx;			// audio format
	// headers
	AVISTREAMINFO vidh;			// header for a single video stream
	AVISTREAMINFO audh;			// header for a single audio stream
	// streams
	PAVISTREAM psVid;			// uncompressed video stream
	PAVISTREAM psVidCompressed;	// compressed video stream
	PAVISTREAM psAud;			// uncompressed audio stream
	PAVISTREAM psAudCompressed;	// compressed audio stream (not supported yet)
	// compression options
	COMPVARS compvar;			// compression options
	AVICOMPRESSOPTIONS opts;	// compression options
	// other
	unsigned int nFrameNum;		// frame number for each compressed frame
	unsigned char* pBitmap;		// buffer for bitmap
	int (*MakeBitmap) ();		// MakeBitmapNoRotate, MakeBitmapRotateCW, MakeBitmapRotateCCW
} FBAvi;

// Opens an avi file for writing.
// Returns: 0 (successful), 1 (failed)
static int AviCreateFile()
{
	swprintf(commonChoice, L"%hs", BurnDrvGetTextA(DRV_NAME));
	if (commonFileDialog(hScrnWnd, true, localiseStringEx(IDS_SELECT_AVIFILE), L"AVI Files (*.avi)\0*.avi\0\0", 0, L"avi")) {
		return 1;
	}

	// use handler determined from file extension)
	HRESULT hRet = AVIFileOpen(&FBAvi.pFile, commonChoice, OF_CREATE | OF_WRITE, NULL);
	if (FAILED(hRet)) {
#ifdef _DEBUG
		printf("    AVI Error: AVIFileOpen() failed.\n");
		switch (hRet) {
			case AVIERR_BADFORMAT:
				printf("\t           Cannot read file (corrupt file or unrecognized format).\n");
				break;
			case AVIERR_MEMORY:
				printf("\t           Cannot open file (insufficient memory).\n");
				break;
			case AVIERR_FILEREAD:
				printf("\t           Cannot read file (disk error).\n");
				break;
			case AVIERR_FILEOPEN:
				printf("\t           Cannot open file (disk error).\n");
				break;
			case REGDB_E_CLASSNOTREG:
				printf("\t           Windows registry does not have a handler for this file type (.avi).\n");
				break;
			default:
				printf("\t           Unknown error).\n");
				break;
		}
#endif
		return 1;
	}

	return 0;
}

// Converts video buffer to bitmap, no rotation
// Returns: 0 (successful), 1 (failed)
static inline int MakeBitmapNoRotate()
{
	if (aviBuffer == NULL) {
		return 1; // video buffer is empty
	}

	int w, h;
	int width = FBAvi.bih.biWidth;
	int height = FBAvi.bih.biHeight;
	unsigned char* temp = FBAvi.pBitmap; // walks through and fills the bitmap

	/*
	initial = rrrrrrrr gggggggg bbbbbbbb xxxxxxxx (8 8 8 x)
	(last 8 bits not used)

	rrrrrrrr gggggggg bbbbbbbbbb xxxxxxxx
	|||||||| |||||||| ||||||||||
	RRRRRRRR GGGGGGGG BBBBBBBBBB

	final = RRRRRRRR GGGGGGGG BBBBBBBB (8 8 8)
	*/

	// start at the buttom line
	unsigned char *p8 = aviBuffer + (width*(height-1)<<2);

	for (h=height-1; h>-1; h--) {
		for (w=width-1; w>-1; w--) {
			memcpy(temp,p8,3); // just copy 24 bits straight into bitmap
			temp += 3; // go to next pixel
			p8 += 4; // go to next pixel
		}
		p8 -= width<<3; // go to next line up
	}

	return 0;
}

// Converts video buffer to bitmap, rotate clockwise
// Returns: 0 (sucessful), 1 (failed), modified by regret
static inline int MakeBitmapRotateCW()
{
	if (aviBuffer == NULL) {
		return 1; // video buffer is empty
	}

	int w, h;
	int width = FBAvi.bih.biWidth;
	int height = FBAvi.bih.biHeight;
	unsigned char* temp = FBAvi.pBitmap; // walks through and fills the bitmap

	// start at upper left corner
	unsigned char *p8 = aviBuffer;

	for (int y = height - 1; y >= 0; y--) {
		for (int x = width - 1; x >= 0; x--) {
			((unsigned int*)temp)[(width - x - 1) + (height - y - 1) * width] = ((unsigned int*)p8)[x + y * width];
		}
	}

	return 0;
}

// Converts video buffer to bitmap, rotate counter clockwise
// Returns: 0 (sucessful), 1 (failed)
static inline int MakeBitmapRotateCCW()
{
	if (aviBuffer == NULL) {
		return 1; // video buffer is empty
	}

	int w, h;
	int width = FBAvi.bih.biWidth;
	int height = FBAvi.bih.biHeight;
	unsigned char* temp = FBAvi.pBitmap; // walks through and fills the bitmap

	// start at upper left corner
	unsigned char *p8 = aviBuffer;
	unsigned char *p8start = p8;

	for (h=1; h<=height; h++) {
		for (w=width-1; w>-1; w--) {
			memcpy(temp,p8,3); // just copy 24 bits straight into bitmap
			temp += 3; // go to next pixel
			p8 += height<<2; // go to next pixel
		}
		p8 = p8start + (h<<2); // go to next column
	}

	return 0;
}

// Converts video buffer to bitmap, rotate counter clockwise for flipped games
// Returns: 0 (sucessful), 1 (failed), modified by regret
static inline int MakeBitmapRotateCCW1()
{
	if (aviBuffer == NULL) {
		return 1; // video buffer is empty
	}

	int w, h;
	int width = FBAvi.bih.biWidth;
	int height = FBAvi.bih.biHeight;
	unsigned char* temp = FBAvi.pBitmap; // walks through and fills the bitmap

	// start at the buttom line
	unsigned char *p8 = aviBuffer + ((width*height-1)<<2);
	unsigned char *p8start = p8;

	for (h=1; h<=height; h++) {
		for (w=width-1; w>-1; w--) {
			memcpy(temp,p8,3); // just copy 24 bits straight into bitmap
			temp += 3; // go to next pixel
			p8 -= height<<2; // go to next pixel
		}
		p8 = p8start - (h<<2); // go to next column
	}

	return 0;
}

// Sets the format for video stream.
static void AviSetVidFormat()
{
	// set the format of bitmap
	memset(&FBAvi.bih, 0, sizeof(BITMAPINFOHEADER));
	FBAvi.bih.biSize = sizeof(BITMAPINFOHEADER);

	unsigned w, h;
	BurnDrvGetVisibleSize(w, h);
	FBAvi.bih.biWidth = w;
	FBAvi.bih.biHeight = h;

	// check for rotation and choose bitmap function
	if ((BurnDrvGetFlags() & BDF_ROTATE_GRAPHICS_CCW)
		&& !(BurnDrvGetFlags() & BDF_ROTATE_GRAPHICS_CW)) {
		// counter-clockwise rotation
		FBAvi.MakeBitmap = MakeBitmapRotateCCW;
	}
	else if ((BurnDrvGetFlags() & BDF_ROTATE_GRAPHICS_CCW)
		&& (BurnDrvGetFlags() & BDF_ROTATE_GRAPHICS_CW)) {
		// counter-clockwise rotation for vertical and flipped games
		FBAvi.MakeBitmap = MakeBitmapRotateCCW1;
	}
	else if (BurnDrvGetFlags() & BDF_ROTATE_GRAPHICS_CW) {
		// clockwise rotation
		FBAvi.MakeBitmap = MakeBitmapRotateCW;
	}
	else {
		// no rotation
		FBAvi.MakeBitmap = MakeBitmapNoRotate;
	}

	FBAvi.bih.biPlanes = 1;
	// use 24 bpp since most compressors support 24 bpp for input
	FBAvi.bih.biBitCount = 24;
	FBAvi.bih.biCompression = BI_RGB; // uncompressed RGB
	FBAvi.bih.biSizeImage = 3 * FBAvi.bih.biWidth * FBAvi.bih.biHeight;
}

// Sets the format for the audio stream.
static void AviSetAudFormat()
{
	memset(&FBAvi.wfx, 0, sizeof(WAVEFORMATEX));
	FBAvi.wfx.cbSize = sizeof(WAVEFORMATEX);
	FBAvi.wfx.wFormatTag = WAVE_FORMAT_PCM;
	FBAvi.wfx.nChannels = 2; // stereo
	FBAvi.wfx.nSamplesPerSec = audSampleRate; // sample rate
	FBAvi.wfx.wBitsPerSample = 16; // 16-bit
	FBAvi.wfx.nBlockAlign = FBAvi.wfx.wBitsPerSample * FBAvi.wfx.nChannels / 8; // bytes per sample
	FBAvi.wfx.nAvgBytesPerSec = FBAvi.wfx.nSamplesPerSec * FBAvi.wfx.nBlockAlign;
}

// Creates the video stream.
// Returns: 0 (successful), 1 (failed)
static int AviCreateVidStream()
{
	int ret;
	HRESULT hRet;

	/*
	Problem:
	Must set the compressor option prior to creating the video
	stream in the file because fccHandler is unknown until the
	compressor is chosen.  However, AVISaveOptions() requires
	the stream to be created prior to the function call.

	Solution:
	Use ICCompressorChoose() to set the	COMPVARS, create the
	video stream in the file using the returned fccHandler,
	manually convert COMPVARS to AVICOMPRESSOPTIONS, and
	then, make the compressed stream.
	*/

	// initialize COMPVARS
	memset(&FBAvi.compvar, 0, sizeof(COMPVARS));
	FBAvi.compvar.cbSize = sizeof(COMPVARS);
	FBAvi.compvar.dwFlags = ICMF_COMPVARS_VALID;
//	FBAvi.compvar.fccHandler = comptypeDIB;	// default compressor = uncompressed full frames
	FBAvi.compvar.fccHandler = 0;			// default compressor = uncompressed full frames
	FBAvi.compvar.lQ = ICQUALITY_DEFAULT;

	// let user choose the compressor and compression options
	ret = ICCompressorChoose(
		hScrnWnd,
		ICMF_CHOOSE_DATARATE | ICMF_CHOOSE_KEYFRAME,
		(LPVOID) &FBAvi.bih, // uncompressed data input format
		NULL,                // no preview window
		&FBAvi.compvar,      // returned info
		"Set video compression option");
	if (!ret) {
#ifdef _DEBUG
		printf("    AVI Error: ICCompressorChoose() failed.\n");
		ICCompressorFree(&FBAvi.compvar);
#endif
		return 1;
	}

	aviFlags |= FBAVI_VID_SET; // flag |= video compression option is set

	// fill in the header for the video stream
	memset(&FBAvi.vidh, 0, sizeof(FBAvi.vidh));
	FBAvi.vidh.fccType               = streamtypeVIDEO;			// stream type
	FBAvi.vidh.fccHandler            = FBAvi.compvar.fccHandler;
	FBAvi.vidh.dwScale               = 100;						// scale
	FBAvi.vidh.dwRate                = BurnGetRefreshRate();	// rate, (fps = dwRate/dwScale)
	FBAvi.vidh.dwSuggestedBufferSize = FBAvi.bih.biSizeImage;

	// set rectangle for stream
	ret = SetRect(
		&FBAvi.vidh.rcFrame,
		0, // x-coordinate of the rectangle's upper left corner
		0, // y-coordinate of the rectangle's upper left corner
		(int) FBAvi.bih.biWidth,
		(int) FBAvi.bih.biHeight);

	if (ret == 0) {
#ifdef _DEBUG
		printf("    AVI Error: SetRect() failed.\n");
#endif
		return 1;
	}

	// create the video stream in the avi file
	hRet = AVIFileCreateStream(
		FBAvi.pFile,	// file pointer
		&FBAvi.psVid,	// returned stream pointer
		&FBAvi.vidh);	// stream header

	if (hRet) {
#ifdef _DEBUG
		printf("    AVI Error: AVIFileCreateStream() failed.\n");
#endif
		return 1;
	}

	// convert COMPVARS to AVICOMPRESSOPTIONS
	memset(&FBAvi.opts, 0, sizeof(FBAvi.opts));
	FBAvi.opts.fccType = streamtypeVIDEO;
	FBAvi.opts.fccHandler = FBAvi.compvar.fccHandler;
	FBAvi.opts.dwKeyFrameEvery = FBAvi.compvar.lKey;
	FBAvi.opts.dwQuality = FBAvi.compvar.lQ;
	FBAvi.opts.dwBytesPerSecond = FBAvi.compvar.lDataRate * 1024L;
	FBAvi.opts.dwFlags = AVICOMPRESSF_VALID |
		(FBAvi.compvar.lDataRate ? AVICOMPRESSF_DATARATE:0L) |
		(FBAvi.compvar.lKey ? AVICOMPRESSF_KEYFRAMES:0L);
	FBAvi.opts.lpFormat = &FBAvi.bih;
	FBAvi.opts.cbFormat = FBAvi.bih.biSize + FBAvi.bih.biClrUsed * sizeof(RGBQUAD);
	FBAvi.opts.lpParms = FBAvi.compvar.lpState;
	FBAvi.opts.cbParms = FBAvi.compvar.cbState;
	FBAvi.opts.dwInterleaveEvery = 0;

	// make the compressed video stream
	hRet = AVIMakeCompressedStream(
		&FBAvi.psVidCompressed,
		FBAvi.psVid,
		&FBAvi.opts,
		NULL);

	if (hRet != AVIERR_OK) {
#ifdef _DEBUG
		printf("    AVI Error: AVIMakeCompressedStream() failed.\n");
		switch (hRet) {
			case AVIERR_NOCOMPRESSOR:
				printf("\t           A suitable compressor cannot be found.\n");
				break;
			case AVIERR_MEMORY:
				printf("\t           Not enough memory to complete the operation.\n");
				break;
			case AVIERR_UNSUPPORTED:
				printf("\t           Compression is not supported for this type of data.\n");
				break;
			default:
				printf("\t           Unknown error.\n");
				break;
		}
#endif
		return 1;
	}

	// set the stream format for compressed video
	hRet = AVIStreamSetFormat(
		FBAvi.psVidCompressed,	// pointer to the opened video output stream
		0,						// starting position
		&FBAvi.bih,				// stream format
		FBAvi.bih.biSize + FBAvi.bih.biClrUsed * sizeof(RGBQUAD)); // format size

	if (hRet) {
#ifdef _DEBUG
		printf("    AVI Error: AVIStreamSetFormat() failed.\n");
#endif
		return 1;
	}

	return 0;
}

// Creates the audio stream.
// Returns: 0 (successful), 1 (failed)
static int AviCreateAudStream()
{
	HRESULT hRet;

	// fill in the header for the audio stream
	//
	// - dwInitialFrames specifies how much to skew the
	//   audio data ahead of the video frames in interleaved
	//   files (typically 0.75 sec).
	//
	memset(&FBAvi.audh, 0, sizeof(FBAvi.audh));
	FBAvi.audh.fccType               = streamtypeAUDIO; // stream type
	FBAvi.audh.dwScale               = FBAvi.wfx.nBlockAlign;
	FBAvi.audh.dwRate                = FBAvi.wfx.nAvgBytesPerSec;
	FBAvi.audh.dwInitialFrames       = 1; // audio skew
	FBAvi.audh.dwSuggestedBufferSize = nBurnSoundLen<<2;
	FBAvi.audh.dwSampleSize          = FBAvi.wfx.nBlockAlign;

	// create the audio stream
	hRet = AVIFileCreateStream(
		FBAvi.pFile,	// file pointer
		&FBAvi.psAud,	// returned stream pointer
		&FBAvi.audh);	// stream header

	if (hRet) {
#ifdef _DEBUG
		printf("    AVI Error: AVIFileCreateStream() failed.\n");
#endif
		return 1;
	}

	// set the format for audio stream
	hRet = AVIStreamSetFormat(
		FBAvi.psAud,			// pointer to the opened audio output stream
		0,						// starting position
		&FBAvi.wfx,				// stream format
		sizeof(WAVEFORMATEX));	// format size

	if (hRet) {
#ifdef _DEBUG
		printf("    AVI Error: AVIStreamSetFormat() failed.\n");
#endif
		return 1;
	}

	return 0;
}


// Records 1 frame worth of data to the output stream
// Returns: 0 (successful), 1 (failed)
int aviRecordFrame(int draw)
{
	HRESULT hRet;

	/*
	When FBA is frame skipping (draw == 0), we don't
	need to	build a new bitmap, nor encode video frame.
	Just increment the frame number.  However, if avi
	is in vid/aud interleaved mode, audio must be recorded
	every frame regardless of frameskip.
	*/
	if (draw) {
		if (FBAvi.MakeBitmap()) {
#ifdef _DEBUG
			printf("    AVI Error: MakeBitmap() failed.\n");
#endif
			return 1;
		}
		aviBuffer = NULL; // empty video buffer

		// compress the bitmap and write to AVI output stream
		hRet = AVIStreamWrite(
			FBAvi.psVidCompressed,	// stream pointer
			FBAvi.nFrameNum,		// time of this frame
			1,						// number to write
			(LPBYTE) FBAvi.pBitmap,	// pointer to data
			FBAvi.bih.biSizeImage,	// size of this frame
			AVIIF_KEYFRAME,			// flags
			NULL,
			NULL);

		if (hRet != AVIERR_OK) {
#ifdef _DEBUG
			printf("    AVI Error: AVIStreamWrite() failed.\n");
#endif
			return 1;
		}
	}

	// interleave audio
	if (aviIntAudio) {
		// write the PCM audio data to AVI output stream
		hRet = AVIStreamWrite(
			FBAvi.psAud,			// stream pointer
			FBAvi.nFrameNum,		// time of this frame
			1,						// number to write
			(LPBYTE) audNextSound,	// pointer to data
			nBurnSoundLen << 2,		// size of data
			AVIIF_KEYFRAME,			// flags
			NULL,
			NULL);

		if (hRet != AVIERR_OK) {
#ifdef _DEBUG
			printf("    AVI Error: AVIStreamWrite() failed.\n");
#endif
			return 1;
		}
	}

	FBAvi.nFrameNum++;
	return 0;
}

// Frees bitmap.
static void FreeBMP()
{
	if (FBAvi.pBitmap) {
		delete [] FBAvi.pBitmap;
		FBAvi.pBitmap = NULL;
	}
}

// Stops AVI recording.
int aviStop()
{
	if (!(aviFlags & FBAVI_VFW_INIT)) {
		return 1;
	}

	if (FBAvi.psVid) {
		// AVIStreamClose(FBAvi.psVid); // obsolete, use AVIStreamRelease() instead
		AVIStreamRelease(FBAvi.psVid);
	}

	if (FBAvi.psVidCompressed) {
		// AVIStreamClose(FBAvi.psVidCompressed); // obsolete, use AVIStreamReleaes() instead
		AVIStreamRelease(FBAvi.psVidCompressed);
	}

	if (FBAvi.psAud) {
		// AVIStreamClose(FBAvi.psAud); // obsolete, use AVIStreamRelease() instead
		AVIStreamRelease(FBAvi.psAud);
	}

	if (FBAvi.pFile) {
		 // AVIFileClose(FBAvi.pFile); // obsolete, use AVIFileRElease instead
		AVIFileRelease(FBAvi.pFile);
	}

	if (aviFlags & FBAVI_VID_SET) {
		ICCompressorFree(&FBAvi.compvar);
	}

	AVIFileExit();
	FreeBMP();

#ifdef _DEBUG
	printf(" ** AVI recording finished.\n");
	printf("    total frames recorded = %u\n", FBAvi.nFrameNum + 1);
#endif
	aviStatus = 0;
	aviFlags = 0;

	memset(&FBAvi, 0, sizeof(FBAVI));
	menuSync(MENUT_FILE);

	return 0;
}

// Starts AVI recording.
// Returns: 0 (successful), 1 (failed)
int aviStart()
{
	if (!driverOkay) {
		return 1;
	}

	// initialize local variables
	memset (&FBAvi, 0, sizeof(FBAVI));

	// initialize avi file library
	// - we need VFW version 1.1 or greater
	if (HIWORD(VideoForWindowsVersion()) < 0x010a){
		// VFW verison is too old, disable AVI recording
		return 1;
	}
	AVIFileInit();

	aviFlags |= FBAVI_VFW_INIT; // avi file library initialized

	// create the avi file
	if (AviCreateFile()) {
		return 1;
	}

	// set video format
	AviSetVidFormat();

	// create the video stream
	if (AviCreateVidStream()) {
		return 1;
	}

	// interleave audio
	if (aviIntAudio) {
		// set audio format
		AviSetAudFormat();
		// create the audio stream
		if (AviCreateAudStream()) {
			return 1;
		}
	}

	// allocate memory for a 24 bpp bitmap
	FBAvi.pBitmap = new unsigned char[FBAvi.bih.biSizeImage];
	if (FBAvi.pBitmap == NULL) {
		return 1; // not enough memory to create allocate bitmap
	}

	// record the first frame
	if (aviRecordFrame(1)) {
		return 1;
	}

	aviStatus = 1; // recording started
	menuSync(MENUT_FILE);
	return 0;
}

void aviSetBuffer(unsigned char* buffer)
{
	aviBuffer = buffer;
}
